import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Kysely, MysqlDialect } from 'kysely'
import { Database } from './old-schema.interface'
import mysql from 'mysql2'
import { DataSource } from 'typeorm'
import { Bot, PremiumResource, Resource, ResourceComment, ResourceStatus, Server } from '@boticord/resources-client'
import { botTags, parseTags, serverTags } from './tags'
import { User, UserLinkTypes } from '@boticord/users-client'
import { DiscordooService } from '@boticord/discordoo'
import { BotLibrary } from '@boticord/consts'
import { REST } from '@discordjs/rest'

const parseLibrary = (library: string): BotLibrary => {
  switch (library.toLowerCase().replace(/[._]/g, '')) {
    case 'discordjs':
      return BotLibrary['Discord.js']
    case 'discordoo':
      return BotLibrary.Discordoo
    case 'eris':
      return BotLibrary.Eris
    case 'javacord':
      return BotLibrary.Javacord
    case 'jda':
      return BotLibrary.JDA
    case 'discordpy':
      return BotLibrary['Discord.py']
    case 'discord4j':
      return BotLibrary.Discord4J
    case 'discordcr':
      return BotLibrary.Discordcr
    case 'discordnet':
      return BotLibrary['Discord.Net']
    case 'discordgo':
      return BotLibrary.DiscordGO
    case 'dsharpplus':
      return BotLibrary.DSharpPlus
    case 'restcord':
    case 'discordia':
    case 'disco':
    case 'discordrb':
    case 'serenity':
    case 'sword':
      return BotLibrary.Other
    default:
      return BotLibrary.NotSpecified
  }
}

export type PartialUser = { avatar?: string; username: string; discriminator: string; id: string }

const resolveCode = (str: string) => {
  const regex = /(((https?)?:\/\/)?discord\.gg\/)?(?<code>[A-Za-z0-9]+)\/?/gm
  const matches = str.matchAll(regex)
  for (const match of matches) {
    return match.groups!.code
  }

  return ''
}

const extractAvatar = (avatar?: string) => {
  if (avatar === undefined) {
    return undefined
  }
  const segments = avatar.split('/')
  return segments[segments.length - 1].split('.')[0]
}

@Injectable()
export class MigratorService implements OnModuleInit {
  private readonly logger = new Logger(MigratorService.name)
  private readonly users = new Map<string, PartialUser | undefined>

  constructor(
    private readonly config: ConfigService,
    private readonly datastore: DataSource,
    private readonly discordoo: DiscordooService,
  ) {
  }

  async testInvite(code: string) {
    const rest = new REST().setToken(this.discordoo.client.token)
    try {
      const invite = await rest.get(`/invites/${code}`) as { guild?: any }
      return !!invite.guild
    } catch {
      return false
    }
  }

  async getUser(id: string, db: Kysely<Database>) {
    if (this.users.has(id)) {
      return this.users.get(id)
    }

    let user: PartialUser | undefined = await this.discordoo.client.users.cache.get(id)
    if (user) {
      this.users.set(id, user)
      return user
    }

    const fromCache = await db.selectFrom('users_cache')
      .selectAll()
      .where('id', '=', id)
      .executeTakeFirst()

    if (fromCache) {
      const segments = fromCache.tag.split('#')
      user = {
        username: segments[0],
        id,
        discriminator: segments[1],
        avatar: fromCache.avatar,
      }

      this.users.set(id, user)
      return user
    }

    this.logger.log(`Fetching user ${id}`)
    try {
      if (this.config.get('FETCH_USERS') === 'FUCKING_NO') throw 'a'
      const u = await this.discordoo.client.users.fetch(id)
      if (u) {
        this.users.set(id, u)
        return u
      }
    } catch(e) {
      this.logger.error(e)
      return undefined
    }
  }

  async migrateShortCodes(db: Kysely<Database>) {
    const start = Date.now()
    this.logger.log('Migrating short codes.')
    const shortCodes = await db.selectFrom('shortCodes').selectAll().execute()
    const userRepo = this.datastore.getRepository(User)
    const regex = /^https?:\/\/boticord\.top\/profile\/(?<id>\d+)\/?$/gm

    for (const shortCode of shortCodes) {
      if (regex.test(shortCode.link)) {
        const segments = shortCode.link.split('/')
        const userId = segments[segments.length - 1]
        const user = await userRepo.findOne({ where: { id: userId } })
        if (user === null) continue
        user.shortDomain = shortCode.code
        await userRepo.save(user)
      }
    }

    this.logger.log(`Migrated short codes in ${Date.now() - start}ms`)
  }

  async migrateUsers(db: Kysely<Database>) {
    const start = Date.now()
    this.logger.log('Migrating users')
    const users = await db.selectFrom('profiles').selectAll().execute()
    const proms: Promise<unknown>[] = []
    const repo = this.datastore.getRepository(User)

    for (const user of users) {
      const p = await repo.findOne({ where: { id: user.id } })
      if (p !== null) continue
      const profile = new User()

      profile.id = user.id
      profile.shortDescription = user.status
      profile.description = user.about
      profile.socials = {
        [UserLinkTypes.Git]: user.git,
        [UserLinkTypes.Vk]: user.vk,
        [UserLinkTypes.Custom]: user.site,
        [UserLinkTypes.Telegram]: undefined,
        [UserLinkTypes.Donate]: undefined,
      }

      const cache = await this.getUser(user.id, db)
      profile.username = cache?.username ?? 'unknown'
      profile.avatar = cache?.avatar
      profile.discriminator = cache?.discriminator ?? '0000'

      this.logger.log(`Migrated user ${user.id}`)
      proms.push(repo.save(profile))
    }

    await Promise.all(proms)
    this.logger.log(`Migrated users in ${Date.now() - start}ms`)
  }

  async migrateServers(db: Kysely<Database>) {
    const start = Date.now()
    this.logger.log('Migrating servers')
    const servers = await db.selectFrom('servers')
      .selectAll()
      .execute()
    const serversBanned = await db.selectFrom('servers_banned')
      .selectAll()
      .execute()
    const serverComments = await db.selectFrom('servers_comments')
      .selectAll()
      .execute()
    const ratings = new Map<string, number[]>

    const isBanned = (id: string) => serversBanned.some(v => v.serverID === id)
    const serversRepo = this.datastore.getRepository(Server)
    let proms: Promise<unknown>[] = []

    for (const server of servers) {
      const serverv2 = new Server()
      serverv2.status = isBanned(server.serverID) ? ResourceStatus.Banned : ResourceStatus.Public
      if (server.status === 0) {
        serverv2.status = ResourceStatus.Hidden
      }
      serverv2.name = server.serverName ?? '<data lost>'
      serverv2.id = server.serverID
      serverv2.bannerURL = server.imageURL
      serverv2.avatar = server.serverAvatar
      serverv2.createdDate = new Date()
      serverv2.memberCount = server.serverMembersAllCount ?? 0
      serverv2.description = server.about
      serverv2.avatar = extractAvatar(server.serverAvatar)
      serverv2.owner = server.serverOwnerID ?? ''
      serverv2.shortDescription = server.description
      serverv2.inviteLink = resolveCode(server.invite!)
      if (serverv2.owner !== '') {
        const user = await this.datastore
          .getRepository(User)
          .findOne({ where: { id: serverv2.owner } })
        if (user) {
          serverv2.moderators = [ user ]
        }
      }
      serverv2.tags = parseTags(server.tags ?? '', serverTags)
      serverv2.ratings = [
        { rating: 1, count: 0 },
        { rating: 2, count: 0 },
        { rating: 3, count: 0 },
        { rating: 4, count: 0 },
        { rating: 5, count: 0 },
      ]
      serverv2.comments = []
      serverv2.boosts = []
      serverv2.ups = []
      proms.push(serversRepo.save(serverv2).catch(console.error))
    }

    await Promise.all(proms)

    const commentsRepo = this.datastore.getRepository(ResourceComment)
    proms = []

    for (const comment of serverComments) {
      if (!ratings.has(comment.serverID)) {
        ratings.set(comment.serverID, [ 0, 0, 0, 0, 0 ])
      }

      const commentv2 = new ResourceComment()
      const resource = new Resource()
      resource.id = comment.serverID
      commentv2.createdDate = new Date(comment.updatedAt)
      commentv2.content = comment.text
      commentv2.modReply = comment.admin_text
      commentv2.resource = resource
      const author = await this.datastore.getRepository(User)
        .findOne({ where: { id: comment.userID } })
      if (author) {
        commentv2.author = author
      }
      commentv2.rating = comment.vote === -1 ? 1 : 5
      ratings.get(comment.serverID)![comment.vote === -1 ? 0 : 4] += 1
      commentv2.reports = []
      proms.push(commentsRepo.save(commentv2).catch(() => null))
    }

    await Promise.all(proms)
    proms = []

    for (const server of servers) {
      try {
        const rating = ratings.get(server.serverID)!
          .map((count, rating) => ({ count, rating: rating + 1 }))
        proms.push(serversRepo.update(server.serverID, {
          ratings: rating,
        }))
      } catch {
        /**/
      }
    }

    await Promise.all(proms)
    this.logger.log(`Migrated servers in ${Date.now() - start}ms`)
  }

  async migrateBots(db: Kysely<Database>) {
    const start = Date.now()
    this.logger.log('Migrating bots.')
    const comments = await db.selectFrom('comments').selectAll().execute()
    const bots = await db.selectFrom('bots').selectAll().execute()
    const botsBanned = await db.selectFrom('bots_banned').selectAll().execute()
    const isBanned = (id: string) => botsBanned.some(e => e.botID === id)
    const botsRepo = this.datastore.getRepository(Bot)
    let proms: Promise<unknown>[] = []

    for (const bot of bots) {
      const p = await botsRepo.findOne({ where: { id: bot.botID } })
      if (p !== null) continue
      const botUser = await this.getUser(bot.botID, db)
      if (botUser === undefined) {
        continue
      }

      const botv2 = new Bot()
      botv2.id = bot.botID
      botv2.tags = parseTags(bot.tags, botTags)
      botv2.createdDate = new Date()
      const developers = bot.developers.split(',').map(e => e.trim())
      botv2.owner = developers[0]
      const userDevelopers: User[] = []
      for (const dev of developers) {
        const user = await this.datastore
          .getRepository(User)
          .findOne({ where: { id: dev } })
        if (user !== null) {
          userDevelopers.push(user)
        } else {
          const user = new User()
          const fromCache = await this.getUser(dev, db)
          if (fromCache) {
            user.id = dev
            user.discriminator = fromCache.discriminator
            user.username = fromCache.username
          } else {
            user.id = dev
            user.discriminator = '0000'
            user.username = 'unknown'
          }
          await this.datastore.getRepository(User).save(user)
          userDevelopers.push(user)
        }
      }

      botv2.developers = userDevelopers
      botv2.discriminator = botUser.discriminator
      botv2.avatar = botUser.avatar
      botv2.name = botUser.username
      botv2.standardBannerID = 0
      botv2.guilds = bot.servers
      botv2.shards = bot.shards
      botv2.library = parseLibrary(bot.library ?? 'afagweerber')
      botv2.members = bot.users
      botv2.shortLink = bot.shortCode
      botv2.prefix = bot.prefix
      const segmentsInvite = bot.discord?.split('/')
      botv2.supportServerInviteCode = segmentsInvite?.[segmentsInvite.length - 1] ?? undefined
      const link = new URL('https://discord.com/oauth2/authorize?scope=bot%20applications.commands')
      link.searchParams.set('client_id', bot.botID)
      link.searchParams.set('permissions', `${bot.perms}`)
      botv2.inviteLink = link.toString()
      botv2.website = bot.site
      botv2.description = bot.about
      botv2.shortDescription = bot.description
      botv2.premium = new PremiumResource()
      botv2.premium.active = bot.bannerInstall === 1
      botv2.premium.bannerURL = bot.imageURL
      botv2.ratings = [
        { rating: 1, count: 0 },
        { rating: 2, count: 0 },
        { rating: 3, count: 0 },
        { rating: 4, count: 0 },
        { rating: 5, count: 0 },
      ]
      if (isBanned(bot.botID)) {
        botv2.status = ResourceStatus.Banned
      } else if (bot.unlisted === 1 || +bot.status !== 3) {
        botv2.status = ResourceStatus.Hidden
      } else {
        botv2.status = ResourceStatus.Public
      }

      proms.push(botsRepo.save(botv2).catch(console.error))
    }

    const ratings = new Map<string, number[]>
    await Promise.all(proms)

    const commentsRepo = this.datastore.getRepository(ResourceComment)
    proms = []

    for (const comment of comments) {
      if (!ratings.has(comment.botID)) {
        ratings.set(comment.botID, [ 0, 0, 0, 0, 0 ])
      }

      const commentv2 = new ResourceComment()
      const resource = new Resource()
      resource.id = comment.botID
      commentv2.createdDate = new Date(comment.updatedAt)
      commentv2.content = comment.text
      commentv2.modReply = comment.admin_text
      commentv2.resource = resource
      commentv2.rating = comment.vote === -1 ? 1 : 5
      const author = await this.datastore.getRepository(User)
        .findOne({ where: { id: comment.userID } })
      if (author) {
        commentv2.author = author
      }
      ratings.get(comment.botID)![comment.vote === -1 ? 0 : 4] += 1
      commentv2.reports = []
      proms.push(commentsRepo.save(commentv2).catch(() => null))
    }

    await Promise.all(proms)
    proms = []

    for (const bot of bots) {
      try {
        const rating = ratings.get(bot.botID)!
          .map((count, rating) => ({ count, rating: rating + 1 }))
        proms.push(botsRepo.update(bot.botID, {
          ratings: rating,
        }))
      } catch {
        /**/
      }
    }

    await Promise.all(proms)

    this.logger.log(`Migrated bots in ${Date.now() - start}ms.`)
  }

  async runMigrations() {
    const db = new Kysely<Database>({
      dialect: new MysqlDialect({
        pool: mysql.createPool({
          host: 'mysql',
          port: 3306,
          database: 'boticord_app',
          user: 'root',
          password: '123',
        }),
      }),
    })
    const time = Date.now()
    // await this.migrateUsers(db)
    await this.migrateBots(db)
    await this.migrateShortCodes(db)
    await this.migrateServers(db)
    this.logger.log(`Finished in ${Date.now() - time}ms`)
  }

  async onModuleInit() {
    const doMigrations = this.config.get<string>('RUN_MIGRATION')

    if (doMigrations !== undefined && doMigrations !== 'no') {
      this.logger.log('Migration mode begins')
      await this.runMigrations()
    } else {
      this.logger.log('Migration mode disabled, disabling this service')
      process.exit(0)
    }
  }
}